package sg.edu.nus.iss.billing.datastore.impl;

import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;

import sg.edu.nus.iss.billing.datastore.AbstractFileStorage;
import sg.edu.nus.iss.billing.datastore.DataStoreHelper;
import sg.edu.nus.iss.billing.datastore.IDataStore;
import sg.edu.nus.iss.billing.datastore.ISelector;
import sg.edu.nus.iss.billing.datastore.MessageConstants;
import sg.edu.nus.iss.billing.datastore.Transaction;
import sg.edu.nus.iss.billing.entity.BaseEntity;
import sg.edu.nus.iss.billing.exception.DataStoreException;
import sg.edu.nus.iss.billing.exception.NoRecordFoundException;

/**
 * This implement is for store object to data file/read object from file
 * How to store data/parse data is based on the implementation of AbstractFileStorage
 * </br>
 * Refer to {@link IDataStore} to get more details
 * </br>
 * @author Wang DaYong
 * 
 */
public class FileDataStoreImpl implements IDataStore {

	protected AbstractFileStorage storage;

	public FileDataStoreImpl(AbstractFileStorage storage) {

		super();
		this.storage = storage;
	}

	@Override
	public <T extends BaseEntity> T loadObjectByID(Type type, String id) throws DataStoreException {

		List<T> list = loadObjectsByType(type);

		// get the index by compare the id
		int index = DataStoreHelper.getIndex(list, id);
		// if found, return the object, else throw no record found exception
		if (index>=0) {
			return list.get(index);
		} else {
			return null;
		}
	}

	@Override
	public <T extends BaseEntity> List<T> loadObjectsByType(Type type) throws DataStoreException {

		List<T> list = loadAll(type);
		return DataStoreHelper.filterByType(list, type);
	}

	
	@SuppressWarnings("unchecked")
	public <T extends BaseEntity> List<T> loadObjectsByIDs(Type type, String... ids) throws DataStoreException {

		ArrayList<T> list = new ArrayList<T>();
		for (String id : ids) {
			Object t = loadObjectByID(type, id);
			list.add((T) t);
		}
		if (list.isEmpty())
			return null;
		return list;
	}

	@Override
	public <T extends BaseEntity> void saveObject(T t) throws DataStoreException {

		if (t==null) {
			return;
		}
		// load all parent child data from datafile/memory
		List<T> list = loadAll(t.getClass());
		if (list==null) {
			list = new ArrayList<T>();
		}
		// add new object
		if (t.getId()==null||t.getId().equals("")) {
			t.setId(this.geneateIncrementID(t.getClass()));
			t.setVersion(System.currentTimeMillis());
			list.add(t);
		} else {

			// find if the object whether existed in database, if exists, update
			// it, or create new one
			int index = DataStoreHelper.getIndex(list, t);
			if (index>=0) {
				// check version and update
				if (t.getVersion().compareTo(list.get(index).getVersion())!=0) {
					throw new DataStoreException(MessageConstants.VERSION_CHECK_UPDATE_ERR);
				} else {
					t.setVersion(System.currentTimeMillis());
				}
				list.set(index, t);
			} else {
				throw new DataStoreException(MessageConstants.VERSION_CHECK_DEL_ERR);

				// in current version, we forbid to set id by ourself, if allow
				// set id manually, should do insert
				// t.setVersion(System.currentTimeMillis());
				// insert
				// list.add(t);
			}
		}

		saveAll(t.getClass(), list);
	}

	@Override
	public <T extends BaseEntity> void saveObjects(List<T> ts) throws DataStoreException {

		if (ts!=null&&!ts.isEmpty()) {
			List<T> list = loadAll(ts.get(0).getClass());
			if (list==null) {
				list = new ArrayList<T>();
			}
			for (T t : ts) {
				if (t.getId()==null||t.getId().equals("")) {
					t.setId(this.geneateIncrementID(t.getClass()));
					t.setVersion(System.currentTimeMillis());
					list.add(t);
				} else {
					int index = DataStoreHelper.getIndex(list, t);
					if (index>=0) {
						// update
						// check version
						if (t.getVersion().compareTo(list.get(index).getVersion())!=0) {
							throw new DataStoreException(MessageConstants.VERSION_CHECK_UPDATE_ERR);
						} else {
							t.setVersion(System.currentTimeMillis());
							list.set(index, t);
						}
					} else {
						throw new DataStoreException(MessageConstants.VERSION_CHECK_DEL_ERR);

						// in current version, we forbid to set id by ourself,
						// if allow set id manually, should do insert
						// t.setVersion(System.currentTimeMillis());
						// insert
						// list.add(t);
					}
				}

			}
			saveAll(ts.get(0).getClass(), list);

		}

	}

	@Override
	public <T extends BaseEntity> void deleteObjectByID(Type type, String id) throws DataStoreException {

		List<T> list = loadObjectsByType(type);
		if (list==null||list.isEmpty()) {
			return;
		}
		int index = DataStoreHelper.getIndex(list, id);
		if (index<0) {
			throw new NoRecordFoundException(MessageConstants.NO_RCD_FOUND_DEL_ERR);
		}

		list.remove(index);
		saveAll(type, list);
	}

	@Override
	public <T extends BaseEntity> void deleteObjects(List<T> ts) throws DataStoreException {

		if (ts!=null&&ts.size()!=0) {
			List<T> list = loadAll(ts.get(0).getClass());
			if (!list.removeAll(ts)) {
				throw new DataStoreException(MessageConstants.BATCH_DEL_ERR);
			}
			saveAll(ts.get(0).getClass(), list);
		}

	}

	@Override
	public <T extends BaseEntity> List<T> loadObjectsBySelector(Type type, ISelector selector) throws DataStoreException {

		List<T> result = loadObjectsByType(type);
		if (result!=null&&!result.isEmpty()) {
			try {
				return selector.filter(result);
			} catch (Exception e) {
				throw new DataStoreException(MessageConstants.LOAD_ERR, e);
			}
		}
		return result;
	}

	
	/**
	 * load object form data file by class type
	 * 
	 * @param <T>
	 * @param t
	 * @return
	 * @return
	 * @throws DataStoreException
	 */
	@SuppressWarnings("unchecked")
	protected <T extends BaseEntity> List<T> loadAll(Type t) throws DataStoreException {

		String className = DataStoreHelper.getSuperClassName((Class<?>) t);
		try {
			return (List<T>) storage.load(className);
		} catch (Exception ex) {
			throw new DataStoreException(MessageConstants.LOAD_ERR, ex);
		}

	}

	/**
	 * save to data file(file name is full class name)
	 * 
	 * @param <T>
	 * @param saveList
	 * @throws DataStoreException
	 */
	protected void saveAll(Type type, List<?> saveList) throws DataStoreException {

		String className = DataStoreHelper.getSuperClassName((Class<?>) type);
		try {
			storage.save(className, saveList);
			if (!storage.isInstantFlush()) {
				storage.flush(className);
			}
		} catch (Exception ex) {
			throw new DataStoreException(MessageConstants.SAVE_ERR, ex);
		}

	}

	/**
	 * generate increment id base on object type, if in batch saving model, will
	 * batch save id changes too
	 * 
	 * @param type
	 * @return
	 * @throws DataStoreException
	 */
	private String geneateIncrementID(final Type type) throws DataStoreException {

		try {
			List<IncrementIDGenerator> list = this.loadAll(IncrementIDGenerator.class);
			if (list==null) {
				list = new ArrayList<IncrementIDGenerator>();
				list.add(new IncrementIDGenerator());
				this.saveAll(IncrementIDGenerator.class, list);
			}
			Integer i = list.get(0).generateID(DataStoreHelper.getSuperClassName((Class<?>) type));
			this.saveAll(IncrementIDGenerator.class, list);
			return String.valueOf(i);
		} catch (DataStoreException e) {
			throw new DataStoreException(MessageConstants.GEN_ID_ERR, e);
		}
	}

	@Override
	public Transaction getTransaction() throws DataStoreException {

		throw new DataStoreException(MessageConstants.TRANS_UNSUPPORT);
	}

	/**
	 * this is just for testing to clear all cache
	 */
	public void clearCache() {

		storage.clearCache();
	}

	@Override
	public void deleteObjectsByType(Type type) throws DataStoreException {
		deleteObjects(loadObjectsByType(type));
	}

}
